package com.jeequan.jeepay.core.aop;


import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializeConfig;
import com.jeequan.jeepay.core.utils.RequestUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.*;

/**
 * 日志切面
 */
@Slf4j
@Aspect
@Configuration
@Component
public class LogAspectAutoConfiguration {

    /**
     * 日志打印长度，默认5000个字符
     */
    private static int PRINT_LOG_LEN = 5000;

    private static String REQUEST_ID = "Request-Id";

    public LogAspectAutoConfiguration() {
        log.info("初始化日志切面...");
    }

    @Pointcut("within(@org.springframework.stereotype.Controller *)")
    public void controller() {
    }

    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
    public void restController() {
    }

    @Pointcut("controller() || restController()")
    public void logAspect() {
    }

    /**
     * Controller层日志拦截
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("logAspect()")
    public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        // 【1】获取简单类名、方法名、方法参数
        String classSimpleName = joinPoint.getTarget().getClass().getSimpleName();
        Signature signature = joinPoint.getSignature();
        Method method = ((MethodSignature) signature).getMethod();
        String methodName = method.getName();
        Object[] args = joinPoint.getArgs();

        // 【2】打印客户端调用方信息
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(
                RequestContextHolder.getRequestAttributes())).getRequest();
        IgnoreLog ignoreLog = method.getAnnotation(IgnoreLog.class);
        String url = request.getRequestURL().toString();
        if (!RequestUtils.isStaticResource(url) && ignoreLog == null) {
            String ip = RequestUtils.getIpAddress();
            log.info("ip：{}\t{}\t{}", ip, request.getMethod(), url);
        }

        // 【3】打印请求参数
        Map<String, Object> map = new LinkedHashMap<>();
        List<Object> argsForLog = getArgsForLog(args);
        for (int i = 0; i < method.getParameterTypes().length; i++) {
            map.put(String.valueOf(i), argsForLog.get(i));
        }
        if (ignoreLog == null) {
            log.info("{}.{} 执行开始，入参：{}", classSimpleName, methodName, serializeSubStr(map));
        }

        // 【4】打印函数调用结果
        Object result = null;
        try {
            result = joinPoint.proceed(args);
            if (ignoreLog == null) {
                log.info("{}.{} 执行成功，耗时{}ms，出参：{}", classSimpleName, methodName, System.currentTimeMillis() - startTime, serializeSubStr(result));
            }
            return result;
        } catch (Exception t) {
            log.error("{}.{} 执行失败，耗时{}ms", classSimpleName, methodName, System.currentTimeMillis() - startTime);
            throw t;
        }
    }

    /**
     * RestTemplate日志拦截
     *
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "execution(* org.springframework.web.client.RestTemplate.*(..))")
    public Object logRestTemplate(ProceedingJoinPoint joinPoint) throws Throwable {

        // 获取方法参数
        String classSimpleName = joinPoint.getSignature().getDeclaringType().getSimpleName();
        String methodName = joinPoint.getSignature().getName();

        // 打印入参日志
        Object[] args = joinPoint.getArgs();
        log.info("{}.{} 执行开始...", classSimpleName, methodName);
        log.debug("输入参数：{}", Arrays.toString(args));

        // 打印出参日志
        long startTime = System.currentTimeMillis();
        Object result;
        try {
            result = joinPoint.proceed();
            long duration = System.currentTimeMillis() - startTime;
            log.info("{}.{} 执行成功，耗时{}ms，出参：{}", classSimpleName, methodName, duration, serializeSubStr(result));
        } catch (RestClientResponseException t) {
            long duration = System.currentTimeMillis() - startTime;
            log.error("{}.{} 执行失败，耗时{}ms，异常为：{}", classSimpleName, methodName, duration, t.getResponseBodyAsString());
            throw t;
        } catch (Exception t) {
            long duration = System.currentTimeMillis() - startTime;
            log.error("{}.{} 执行失败，耗时{}ms，异常为：{}", classSimpleName, methodName, duration, t.getMessage());
            throw t;
        }
        return result;
    }


    /**
     * 获取入参列表
     *
     * @param args
     * @return
     */
    private List<Object> getArgsForLog(Object[] args) {
        List<Object> argsForLog = new ArrayList<>();
        for (Object arg : args) {
            if (arg == null) {
                argsForLog.add(null);
                continue;
            }

            if (ServletRequest.class.isAssignableFrom(arg.getClass()) || ServletResponse.class.isAssignableFrom(arg.getClass())
                    || HttpSession.class.isAssignableFrom(arg.getClass()) || MultipartFile.class.isAssignableFrom(arg.getClass())) {
                argsForLog.add(arg.getClass());
                continue;
            }

            if (Errors.class.isAssignableFrom(arg.getClass())) {
                Map<String, Object> map = new LinkedHashMap<>();
                for (FieldError fieldError : ((Errors) arg).getFieldErrors()) {
                    map.put(fieldError.getField(),
                            fieldError.getDefaultMessage() + ";" + ObjectUtils.defaultIfNull(map.get(fieldError.getField()), ""));
                }
                argsForLog.add(map.isEmpty() ? arg.getClass() : map);
                continue;
            }
            argsForLog.add(arg);
        }
        return argsForLog;
    }

    /**
     * 日志打印时，数据脱敏，并防止数据过大时截取2000字符串打印
     *
     * @param data
     */
    private Object serializeSubStr(Object data) {
        // 如果对象为空或者无法序列化，直接返回
        if (data == null || !Serializable.class.isAssignableFrom(data.getClass())) {
            return data;
        }

        // 序列化的时候关闭ASM
        SerializeConfig.getGlobalInstance().setAsmEnable(false);
        // 反序列化的时候关闭ASM
        ParserConfig.getGlobalInstance().setAsmEnable(false);

        data = JSONObject.parseObject(JSONObject.toJSONString(data), Object.class);
        if (JSONArray.class.isAssignableFrom(data.getClass())) {
            for (Object item : (JSONArray) data) {
                serializeSubStr(item);
            }
        } else if (JSONObject.class.isAssignableFrom(data.getClass())) {
            for (Map.Entry<String, Object> item : ((JSONObject) data).entrySet()) {
                item.setValue(serializeSubStr(item.getValue()));
            }
        } else if (String.class.isAssignableFrom(data.getClass())) {
            String dataStr = String.valueOf(data);
            if (dataStr.length() > PRINT_LOG_LEN) {
                log.warn("参数字段长度超过{}字符，日志输出时已做截取，请知", PRINT_LOG_LEN);
                data = dataStr.substring(0, PRINT_LOG_LEN);
            }
        }
        return data;
    }

}
